#include "StructWidget.h"

#include <QFileDialog>

#include "Configuration.h"
#include "GotoDialog.h"
#include "LineEditDialog.h"
#include "MenuBuilder.h"
#include "MiscUtil.h"
#include "StringUtil.h"
#include "ui_StructWidget.h"

struct TypeDescriptor {
  TYPEDESCRIPTOR type;
  QString name;
};
Q_DECLARE_METATYPE(TypeDescriptor)

StructWidget::StructWidget(QWidget* parent)
    : QWidget(parent), ui(new Ui::StructWidget) {
  ui->setupUi(this);
  ui->treeWidget->setStyleSheet(
      "QTreeWidget { color: #000000; background-color: #FFF8F0; "
      "alternate-background-color: #DCD9CF; }");
  connect(Bridge::getBridge(),
          SIGNAL(typeAddNode(void*, const TYPEDESCRIPTOR*)), this,
          SLOT(typeAddNode(void*, const TYPEDESCRIPTOR*)));
  connect(Bridge::getBridge(), SIGNAL(typeClear()), this, SLOT(typeClear()));
  connect(Bridge::getBridge(), SIGNAL(typeUpdateWidget()), this,
          SLOT(typeUpdateWidget()));
  connect(Bridge::getBridge(), SIGNAL(dbgStateChanged(DBGSTATE)), this,
          SLOT(dbgStateChangedSlot(DBGSTATE)));
  connect(Config(), SIGNAL(colorsUpdated()), this, SLOT(colorsUpdatedSlot()));
  connect(Config(), SIGNAL(fontsUpdated()), this, SLOT(fontsUpdatedSlot()));
  connect(Config(), SIGNAL(shortcutsUpdated()), this,
          SLOT(shortcutsUpdatedSlot()));
  colorsUpdatedSlot();
  fontsUpdatedSlot();
  setupContextMenu();
  setupColumns();
}

StructWidget::~StructWidget() { delete ui; }

void StructWidget::colorsUpdatedSlot() {
  auto color = ConfigColor("AbstractTableViewTextColor");
  auto background = ConfigColor("StructBackgroundColor");
  auto altBackground = ConfigColor("StructAlternateBackgroundColor");
  auto style = QString(
                   "QTreeWidget { color: %1; background-color: %2; "
                   "alternate-background-color: %3; }")
                   .arg(color.name(), background.name(), altBackground.name());
  ui->treeWidget->setStyleSheet(style);
}

void StructWidget::fontsUpdatedSlot() {
  auto font = ConfigFont("AbstractTableView");
  setFont(font);
  ui->treeWidget->setFont(font);
  ui->treeWidget->header()->setFont(font);
}

void StructWidget::shortcutsUpdatedSlot() { updateShortcuts(); }

void StructWidget::typeAddNode(void* parent, const TYPEDESCRIPTOR* type) {
  TypeDescriptor dtype;
  dtype.type = *type;
  dtype.name = QString(dtype.type.name);
  dtype.type.name = nullptr;
  auto text = QStringList()
              << dtype.name << ToPtrString(dtype.type.addr + dtype.type.offset)
              << "0x" + ToHexString(dtype.type.size);
  QTreeWidgetItem* item =
      parent ? new QTreeWidgetItem((QTreeWidgetItem*)parent, text)
             : new QTreeWidgetItem(ui->treeWidget, text);
  item->setExpanded(dtype.type.expanded);
  QVariant var;
  var.setValue(dtype);
  item->setData(0, Qt::UserRole, var);
  Bridge::getBridge()->setResult(BridgeResult::TypeAddNode, dsint(item));
}

void StructWidget::typeClear() {
  ui->treeWidget->clear();
  Bridge::getBridge()->setResult(BridgeResult::TypeClear);
}

void StructWidget::typeUpdateWidget() {
  for (QTreeWidgetItemIterator it(ui->treeWidget); *it; ++it) {
    QTreeWidgetItem* item = *it;
    auto type = item->data(0, Qt::UserRole).value<TypeDescriptor>();
    auto name = type.name.toUtf8();
    type.type.name = name.constData();
    auto addr = type.type.addr + type.type.offset;
    item->setText(1, ToPtrString(addr));
    QString valueStr;
    if (type.type.callback)  // use the provided callback
    {
      char value[128] = "";
      size_t valueCount = _countof(value);
      if (!type.type.callback(&type.type, value, &valueCount) && valueCount &&
          valueCount != _countof(value)) {
        auto dest = new char[valueCount];
        if (type.type.callback(&type.type, dest, &valueCount))
          valueStr = value;
        else
          valueStr = "???";
        delete[] dest;
      } else
        valueStr = value;
    } else if (!item->childCount() && type.type.size > 0 &&
               type.type.size <= sizeof(uint64_t))  // attempt to display small,
                                                    // non-parent values
    {
      uint64_t data;
      if (DbgMemRead(addr, &data, type.type.size)) {
        if (type.type.reverse)
          std::reverse((char*)data, (char*)data + type.type.size);
        valueStr = QString().sprintf("0x%llX, %llu", data, data, data);
      } else if (type.type.addr)
        valueStr = "???";
    }
    item->setText(3, valueStr);
  }
}

void StructWidget::dbgStateChangedSlot(DBGSTATE state) {
  if (state == stopped) ui->treeWidget->clear();
}

void StructWidget::setupColumns() {
  ui->treeWidget->setColumnWidth(0, 300);  // Name
  ui->treeWidget->setColumnWidth(1, 80);   // Address
  ui->treeWidget->setColumnWidth(2, 80);   // Size
  // ui->treeWidget->setColumnWidth(3, 80); //Value
}

#define hasSelection !!ui->treeWidget->selectedItems().count()
#define selectedItem ui->treeWidget->selectedItems()[0]
#define selectedType \
  selectedItem->data(0, Qt::UserRole).value<TypeDescriptor>().type

void StructWidget::setupContextMenu() {
  mMenuBuilder = new MenuBuilder(this);
  mMenuBuilder->addAction(
      makeAction(DIcon("dump.png"), tr("&Follow in Dump"),
                 SLOT(followDumpSlot())),
      [this](QMenu*) {
        return hasSelection &&
               DbgMemIsValidReadPtr(selectedType.addr + selectedType.offset);
      });
  mMenuBuilder->addAction(
      makeAction(DIcon("structaddr.png"), tr("Change address"),
                 SLOT(changeAddrSlot())),
      [this](QMenu*) {
        return hasSelection && !selectedItem->parent() && DbgIsDebugging();
      });
  mMenuBuilder->addAction(makeAction(DIcon("visitstruct.png"), tr("Visit type"),
                                     SLOT(visitSlot())));
  mMenuBuilder->addAction(makeAction(DIcon("database-import.png"),
                                     tr("Load JSON"), SLOT(loadJsonSlot())));
  mMenuBuilder->addAction(makeAction(DIcon("source.png"), tr("Parse header"),
                                     SLOT(parseFileSlot())));
  mMenuBuilder->addAction(
      makeAction(DIcon("removestruct.png"), tr("Remove"), SLOT(removeSlot())),
      [this](QMenu*) { return hasSelection && !selectedItem->parent(); });
  mMenuBuilder->addAction(
      makeAction(DIcon("eraser.png"), tr("Clear"), SLOT(clearSlot())));
  mMenuBuilder->addAction(makeShortcutAction(
      DIcon("sync.png"), tr("&Refresh"), SLOT(refreshSlot()), "ActionRefresh"));
  mMenuBuilder->loadFromConfig();
}

void StructWidget::on_treeWidget_customContextMenuRequested(const QPoint& pos) {
  QMenu wMenu;
  mMenuBuilder->build(&wMenu);
  if (wMenu.actions().count())
    wMenu.exec(ui->treeWidget->viewport()->mapToGlobal(pos));
}

void StructWidget::followDumpSlot() {
  if (!hasSelection) return;
  DbgCmdExec(QString("dump %1").arg(
      ToPtrString(selectedType.addr + selectedType.offset)));
}

void StructWidget::clearSlot() { ui->treeWidget->clear(); }

void StructWidget::removeSlot() {
  if (!hasSelection) return;
  delete selectedItem;
}

void StructWidget::visitSlot() {
  // TODO: replace with a list to pick from
  LineEditDialog mLineEdit(this);
  mLineEdit.setWindowTitle(tr("Type to visit"));
  if (mLineEdit.exec() != QDialog::Accepted || !mLineEdit.editText.length())
    return;
  if (!mGotoDialog) mGotoDialog = new GotoDialog(this);
  duint addr = 0;
  mGotoDialog->setWindowTitle(tr("Address to visit"));
  if (DbgIsDebugging() && mGotoDialog->exec() == QDialog::Accepted)
    addr = DbgValFromString(mGotoDialog->expressionText.toUtf8().constData());
  DbgCmdExec(
      QString("VisitType %1, %2").arg(mLineEdit.editText, ToPtrString(addr)));
}

void StructWidget::loadJsonSlot() {
  auto filename =
      QFileDialog::getOpenFileName(this, tr("Load JSON"), QString(),
                                   tr("JSON files (*.json);;All files (*.*)"));
  if (!filename.length()) return;
  filename = QDir::toNativeSeparators(filename);
  DbgCmdExec(QString("LoadTypes \"%1\"").arg(filename));
}

void StructWidget::parseFileSlot() {
  auto filename = QFileDialog::getOpenFileName(
      this, tr("Parse header"), QString(),
      tr("Header files (*.h *.hpp);;All files (*.*)"));
  if (!filename.length()) return;
  filename = QDir::toNativeSeparators(filename);
  DbgCmdExec(QString("ParseTypes \"%1\"").arg(filename));
}

static void changeTypeAddr(QTreeWidgetItem* item, duint addr) {
  auto changeAddr =
      item->data(0, Qt::UserRole).value<TypeDescriptor>().type.addr;
  for (QTreeWidgetItemIterator it(item); *it; ++it) {
    QTreeWidgetItem* item = *it;
    auto type = item->data(0, Qt::UserRole).value<TypeDescriptor>();
    type.type.addr = type.type.addr == changeAddr
                         ? addr
                         : 0;  // invalidate pointers (requires revisit)
    QVariant var;
    var.setValue(type);
    item->setData(0, Qt::UserRole, var);
  }
}

void StructWidget::changeAddrSlot() {
  if (!hasSelection || !DbgIsDebugging()) return;
  if (!mGotoDialog) mGotoDialog = new GotoDialog(this);
  mGotoDialog->setWindowTitle(tr("Change address"));
  if (mGotoDialog->exec() != QDialog::Accepted) return;
  changeTypeAddr(
      selectedItem,
      DbgValFromString(mGotoDialog->expressionText.toUtf8().constData()));
  refreshSlot();
}

void StructWidget::refreshSlot() { typeUpdateWidget(); }
